require_relative 'bottle.rb'
require_relative 'pid_controller.rb'

##
# This class is used to quickly generate test situation for
# PID Controller to figure out the Kp, Ki, Kd parameters.
#
# To get a instance of +TempSystem+, you need to pass in
# a +bottle+ and a +controller+. Other possible options
# can be seen at +initialize+ definition.
class TempSystem
  # 
  def initialize(bottle, controller, opts = {})
    @bottle, @controller = bottle, controller
    @collected_data = [] # collected data
    @logger_count = 0 # count how many frame simulated in a period

    # For +logger+ you can pass in +Array+ or +Proc+,
    # * Pass in +Array+ will be converted to a list of methods
    #   applied to bottle to get the returned value.
    #   For example, you can pass in +[:t, :temp]+ to
    #   get a Proc which returns data as +[b.t, b.temp]+.
    # * Pass in +Proc+, the proc should take 2 arguments,
    #   the first one is +bottle+ and the last one is +controller+.
    @logger = if opts[:logger].is_a? Array
                row = opts[:logger].map(&:to_sym)
                -> (b, c) { row.map { |method| b.send(method) } }
              elsif opts[:logger].respond_to? :call
                opts[:loagger]
              else
                -> (b, c) { [b.t, b.temp] }
              end
    # (opts[:logger] || -> (b, c) { [b.t, b.temp] }) # logger method
    @formatter = (opts[:formatter] || -> (num) { num.round(2) }) # logger data formatter
    @frame = (opts[:frame] || 100) # store data every +@frame+
    @bottle.refresh = (opts[:refresh] || -1) # preserve bottle refresh
  end

  def inspect_data(from = nil, to = nil)
    @collected_data[from..to].map { |row| row.join("\t") }.join("\n")
  end

  # simulate for +frame+ frames
  def simulate(frame)
    frame.times { update }
    @collected_data
  end

  # simulate a single frame
  def update
    # log data to +@collected_data+
    if @logger_count == 0
      @collected_data << @logger[@bottle, @controller].map(&@formatter)
    end
    
    # simulate the bottle
    @bottle.update do |bottle|
      err = bottle.expect_temp - bottle.temp
      @controller.update(err, dt: bottle.dt) { |watt| bottle.watt = watt }
    end

    # finish single frame simulate
    @logger_count += 1
    @logger_count = @logger_count % @frame
  end

  # Clear the cache to save space
  def clear_cache
    @collected_data = []
  end
end
